{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Running membership inference attacks on the Nursery data"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "In this tutorial we will show how to run black-box membership attacks. This will be demonstrated on the Nursery dataset (original dataset can be found here: https://archive.ics.uci.edu/ml/datasets/nursery). "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We have already preprocessed the dataset such that all categorical features are one-hot encoded, and the data was scaled using sklearn's StandardScaler."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import os\n",
    "import sys\n",
    "sys.path.insert(0, os.path.abspath('..'))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Load data"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [],
   "source": [
    "from art.utils import load_nursery\n",
    "\n",
    "(x_train, y_train), (x_test, y_test), _, _ = load_nursery(test_set=0.5)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Train random forest model"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Base model accuracy:  0.9739117011423278\n"
     ]
    }
   ],
   "source": [
    "from sklearn.ensemble import RandomForestClassifier\n",
    "from art.estimators.classification.scikitlearn import ScikitlearnRandomForestClassifier\n",
    "\n",
    "model = RandomForestClassifier()\n",
    "model.fit(x_train, y_train)\n",
    "\n",
    "art_classifier = ScikitlearnRandomForestClassifier(model)\n",
    "\n",
    "print('Base model accuracy: ', model.score(x_test, y_test))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Attack\n",
    "### Rule-based attack\n",
    "The rule-based attack uses the simple rule to determine membership in the training data: if the model's prediction for a sample is correct, then it is a member. Otherwise, it is not a member."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Members Accuracy: 1.0000\n",
      "Non Members Accuracy 0.0261\n",
      "Attack Accuracy 0.5130\n"
     ]
    }
   ],
   "source": [
    "import numpy as np\n",
    "from art.attacks.inference.membership_inference import MembershipInferenceBlackBoxRuleBased\n",
    "\n",
    "attack = MembershipInferenceBlackBoxRuleBased(art_classifier)\n",
    "\n",
    "# infer attacked feature\n",
    "inferred_train = attack.infer(x_train, y_train)\n",
    "inferred_test = attack.infer(x_test, y_test)\n",
    "\n",
    "# check accuracy\n",
    "train_acc = np.sum(inferred_train) / len(inferred_train)\n",
    "test_acc = 1 - (np.sum(inferred_test) / len(inferred_test))\n",
    "acc = (train_acc * len(inferred_train) + test_acc * len(inferred_test)) / (len(inferred_train) + len(inferred_test))\n",
    "print(f\"Members Accuracy: {train_acc:.4f}\")\n",
    "print(f\"Non Members Accuracy {test_acc:.4f}\")\n",
    "print(f\"Attack Accuracy {acc:.4f}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "This means that on average for 51% of the data, membership status is inferred correctly."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "(0.50660827402831, 1.0)\n"
     ]
    }
   ],
   "source": [
    "def calc_precision_recall(predicted, actual, positive_value=1):\n",
    "    score = 0  # both predicted and actual are positive\n",
    "    num_positive_predicted = 0  # predicted positive\n",
    "    num_positive_actual = 0  # actual positive\n",
    "    for i in range(len(predicted)):\n",
    "        if predicted[i] == positive_value:\n",
    "            num_positive_predicted += 1\n",
    "        if actual[i] == positive_value:\n",
    "            num_positive_actual += 1\n",
    "        if predicted[i] == actual[i]:\n",
    "            if predicted[i] == positive_value:\n",
    "                score += 1\n",
    "    \n",
    "    if num_positive_predicted == 0:\n",
    "        precision = 1\n",
    "    else:\n",
    "        precision = score / num_positive_predicted  # the fraction of predicted “Yes” responses that are correct\n",
    "    if num_positive_actual == 0:\n",
    "        recall = 1\n",
    "    else:\n",
    "        recall = score / num_positive_actual  # the fraction of “Yes” responses that are predicted correctly\n",
    "\n",
    "    return precision, recall\n",
    "\n",
    "# rule-based\n",
    "print(calc_precision_recall(np.concatenate((inferred_train, inferred_test)), \n",
    "                            np.concatenate((np.ones(len(inferred_train)), np.zeros(len(inferred_test))))))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Black-box attack\n",
    "The black-box attack basically trains an additional classifier (called the attack model) to predict the membership status of a sample. It can use as input to the learning process probabilities/logits or losses, depending on the type of model and provided configuration.\n",
    "#### Train attack model"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [],
   "source": [
    "from art.attacks.inference.membership_inference import MembershipInferenceBlackBox\n",
    "\n",
    "attack_train_ratio = 0.5\n",
    "attack_train_size = int(len(x_train) * attack_train_ratio)\n",
    "attack_test_size = int(len(x_test) * attack_train_ratio)\n",
    "\n",
    "bb_attack = MembershipInferenceBlackBox(art_classifier)\n",
    "\n",
    "# train attack model\n",
    "bb_attack.fit(x_train[:attack_train_size], y_train[:attack_train_size],\n",
    "              x_test[:attack_test_size], y_test[:attack_test_size])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Infer membership and check accuracy"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {
    "scrolled": true
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Members Accuracy: 0.7076\n",
      "Non Members Accuracy 0.5906\n",
      "Attack Accuracy 0.6491\n"
     ]
    }
   ],
   "source": [
    "# get inferred values\n",
    "inferred_train_bb = bb_attack.infer(x_train[attack_train_size:], y_train[attack_train_size:])\n",
    "inferred_test_bb = bb_attack.infer(x_test[attack_test_size:], y_test[attack_test_size:])\n",
    "# check accuracy\n",
    "train_acc = np.sum(inferred_train_bb) / len(inferred_train_bb)\n",
    "test_acc = 1 - (np.sum(inferred_test_bb) / len(inferred_test_bb))\n",
    "acc = (train_acc * len(inferred_train_bb) + test_acc * len(inferred_test_bb)) / (len(inferred_train_bb) + len(inferred_test_bb))\n",
    "print(f\"Members Accuracy: {train_acc:.4f}\")\n",
    "print(f\"Non Members Accuracy {test_acc:.4f}\")\n",
    "print(f\"Attack Accuracy {acc:.4f}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Achieves much better results than the rule-based attack."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "(0.6334991708126037, 0.7076258104353196)\n"
     ]
    }
   ],
   "source": [
    "# black-box\n",
    "print(calc_precision_recall(np.concatenate((inferred_train_bb, inferred_test_bb)), \n",
    "                            np.concatenate((np.ones(len(inferred_train_bb)), np.zeros(len(inferred_test_bb))))))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Train neural network model"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Base model Train accuracy:  1.0\n",
      "Base model Test accuracy:  0.928\n"
     ]
    }
   ],
   "source": [
    "import torch\n",
    "from torch import nn, optim\n",
    "from torch.utils.data import DataLoader\n",
    "from torch.utils.data.dataset import Dataset\n",
    "from art.estimators.classification.pytorch import PyTorchClassifier\n",
    "\n",
    "# reduce size of training set to make attack slightly better\n",
    "train_set_size = 500\n",
    "x_train = x_train[:train_set_size]\n",
    "y_train = y_train[:train_set_size]\n",
    "x_test = x_test[:train_set_size]\n",
    "y_test = y_test[:train_set_size]\n",
    "attack_train_size = int(len(x_train) * attack_train_ratio)\n",
    "attack_test_size = int(len(x_test) * attack_train_ratio)\n",
    "\n",
    "class ModelToAttack(nn.Module):\n",
    "\n",
    "    def __init__(self, num_classes, num_features):\n",
    "        super(ModelToAttack, self).__init__()\n",
    "\n",
    "        self.fc1 = nn.Sequential(\n",
    "                nn.Linear(num_features, 1024),\n",
    "                nn.Tanh(), )\n",
    "\n",
    "        self.fc2 = nn.Sequential(\n",
    "                nn.Linear(1024, 512),\n",
    "                nn.Tanh(), )\n",
    "\n",
    "        self.fc3 = nn.Sequential(\n",
    "            nn.Linear(512, 256),\n",
    "            nn.Tanh(), )\n",
    "        \n",
    "        self.fc4 = nn.Sequential(\n",
    "            nn.Linear(256, 128),\n",
    "            nn.Tanh(),\n",
    "        )\n",
    "\n",
    "        self.classifier = nn.Linear(128, num_classes)\n",
    "\n",
    "    def forward(self, x):\n",
    "        out = self.fc1(x)\n",
    "        out = self.fc2(out)\n",
    "        out = self.fc3(out)\n",
    "        out = self.fc4(out)\n",
    "        return self.classifier(out)\n",
    "\n",
    "mlp_model = ModelToAttack(4, 24)\n",
    "mlp_model = torch.nn.DataParallel(mlp_model)\n",
    "criterion = nn.CrossEntropyLoss()\n",
    "optimizer = optim.Adam(mlp_model.parameters(), lr=0.01)\n",
    "\n",
    "class NurseryDataset(Dataset):\n",
    "    def __init__(self, x, y=None):\n",
    "        self.x = torch.from_numpy(x.astype(np.float64)).type(torch.FloatTensor)\n",
    "\n",
    "        if y is not None:\n",
    "            self.y = torch.from_numpy(y.astype(np.int8)).type(torch.LongTensor)\n",
    "        else:\n",
    "            self.y = torch.zeros(x.shape[0])\n",
    "\n",
    "    def __len__(self):\n",
    "        return len(self.x)\n",
    "\n",
    "    def __getitem__(self, idx):\n",
    "        if idx >= len(self.x):\n",
    "            raise IndexError(\"Invalid Index\")\n",
    "\n",
    "        return self.x[idx], self.y[idx]\n",
    "\n",
    "train_set = NurseryDataset(x_train, y_train)\n",
    "train_loader = DataLoader(train_set, batch_size=100, shuffle=True, num_workers=0)\n",
    "\n",
    "for epoch in range(20):\n",
    "    for (input, targets) in train_loader:\n",
    "        input, targets = torch.autograd.Variable(input), torch.autograd.Variable(targets)\n",
    "\n",
    "        optimizer.zero_grad()\n",
    "        outputs = mlp_model(input)\n",
    "        loss = criterion(outputs, targets)\n",
    "\n",
    "        loss.backward()\n",
    "        optimizer.step()\n",
    "\n",
    "        \n",
    "        \n",
    "mlp_art_model = PyTorchClassifier(model=mlp_model, loss=criterion, optimizer=optimizer, input_shape=(24,), nb_classes=4)\n",
    "\n",
    "train_pred = np.array([np.argmax(arr) for arr in mlp_art_model.predict(x_train.astype(np.float32))])\n",
    "print('Base model Train accuracy: ', np.sum(train_pred == y_train) / len(y_train))\n",
    "\n",
    "test_pred = np.array([np.argmax(arr) for arr in mlp_art_model.predict(x_test.astype(np.float32))])\n",
    "print('Base model Test accuracy: ', np.sum(test_pred == y_test) / len(y_test))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Rule-based attack"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "1.0\n",
      "0.07199999999999995\n",
      "0.536\n",
      "(0.5186721991701245, 1.0)\n"
     ]
    }
   ],
   "source": [
    "mlp_attack = MembershipInferenceBlackBoxRuleBased(mlp_art_model)\n",
    "\n",
    "# infer \n",
    "mlp_inferred_train = mlp_attack.infer(x_train.astype(np.float32), y_train)\n",
    "mlp_inferred_test = mlp_attack.infer(x_test.astype(np.float32), y_test)\n",
    "\n",
    "# check accuracy\n",
    "mlp_train_acc = np.sum(mlp_inferred_train) / len(mlp_inferred_train)\n",
    "mlp_test_acc = 1 - (np.sum(mlp_inferred_test) / len(mlp_inferred_test))\n",
    "mlp_acc = (mlp_train_acc * len(mlp_inferred_train) + mlp_test_acc * len(mlp_inferred_test)) / (len(mlp_inferred_train) + len(mlp_inferred_test))\n",
    "print(mlp_train_acc)\n",
    "print(mlp_test_acc)\n",
    "print(mlp_acc)\n",
    "\n",
    "print(calc_precision_recall(np.concatenate((mlp_inferred_train, mlp_inferred_test)), \n",
    "                            np.concatenate((np.ones(len(mlp_inferred_train)), np.zeros(len(mlp_inferred_test))))))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Black-box attack"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Members Accuracy: 0.4800\n",
      "Non Members Accuracy 0.5240\n",
      "Attack Accuracy 0.5020\n",
      "(0.502092050209205, 0.48)\n"
     ]
    }
   ],
   "source": [
    "mlp_attack_bb = MembershipInferenceBlackBox(mlp_art_model, attack_model_type='rf')\n",
    "\n",
    "# train attack model\n",
    "mlp_attack_bb.fit(x_train[:attack_train_size].astype(np.float32), y_train[:attack_train_size],\n",
    "              x_test[:attack_test_size].astype(np.float32), y_test[:attack_test_size])\n",
    "\n",
    "# infer \n",
    "mlp_inferred_train_bb = mlp_attack_bb.infer(x_train[attack_train_size:].astype(np.float32), y_train[attack_train_size:])\n",
    "mlp_inferred_test_bb = mlp_attack_bb.infer(x_test[attack_test_size:].astype(np.float32), y_test[attack_test_size:])\n",
    "\n",
    "# check accuracy\n",
    "mlp_train_acc_bb = np.sum(mlp_inferred_train_bb) / len(mlp_inferred_train_bb)\n",
    "mlp_test_acc_bb = 1 - (np.sum(mlp_inferred_test_bb) / len(mlp_inferred_test_bb))\n",
    "mlp_acc_bb = (mlp_train_acc_bb * len(mlp_inferred_train_bb) + mlp_test_acc_bb * len(mlp_inferred_test_bb)) / (len(mlp_inferred_train_bb) + len(mlp_inferred_test_bb))\n",
    "\n",
    "print(f\"Members Accuracy: {mlp_train_acc_bb:.4f}\")\n",
    "print(f\"Non Members Accuracy {mlp_test_acc_bb:.4f}\")\n",
    "print(f\"Attack Accuracy {mlp_acc_bb:.4f}\")\n",
    "\n",
    "print(calc_precision_recall(np.concatenate((mlp_inferred_train_bb, mlp_inferred_test_bb)), \n",
    "                            np.concatenate((np.ones(len(mlp_inferred_train_bb)), np.zeros(len(mlp_inferred_test_bb))))))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "              precision    recall  f1-score   support\n",
      "\n",
      "         0.0       0.50      0.52      0.51       250\n",
      "         1.0       0.50      0.48      0.49       250\n",
      "\n",
      "    accuracy                           0.50       500\n",
      "   macro avg       0.50      0.50      0.50       500\n",
      "weighted avg       0.50      0.50      0.50       500\n",
      "\n"
     ]
    }
   ],
   "source": [
    "from sklearn.metrics import classification_report \n",
    "\n",
    "y_train_pred = np.concatenate((mlp_inferred_train_bb, mlp_inferred_test_bb)) \n",
    "y_train_true = np.concatenate((np.ones_like(mlp_inferred_train_bb), np.zeros_like(mlp_inferred_test_bb)))\n",
    "print(classification_report(y_pred=y_train_pred, y_true=y_train_true))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "For the pytorch target model we were able to achieve slightly better than random attack performance, but not as good as for the random forest model."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Worst Case vs Average Case evaluation for Black-Box Attack"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "tpr=0.008: fpr=0.008: threshold=0.98\n"
     ]
    }
   ],
   "source": [
    "from art.metrics.privacy.worst_case_mia_score import get_roc_for_fpr\n",
    "\n",
    "# we run the worst case metric on trainset to find an appropriate threshold\n",
    "# Black Box\n",
    "bb_members_test_prob = mlp_attack_bb.infer(x_train[attack_train_size:].astype(np.float32), y_train[attack_train_size:], probabilities=True)\n",
    "bb_nonmembers_test_prob = mlp_attack_bb.infer(x_test[attack_test_size:].astype(np.float32), y_test[attack_test_size:], probabilities=True)\n",
    "\n",
    "bb_mia_test_probs = np.concatenate((np.squeeze(bb_members_test_prob, axis=-1),\n",
    "                               np.squeeze(bb_nonmembers_test_prob, axis=-1)))\n",
    "                              \n",
    "bb_mia_test_labels = np.concatenate((np.ones_like(y_train[:attack_train_size]), np.zeros_like(y_test[:attack_test_size])))\n",
    "# We allow 1% FPR \n",
    "fpr, tpr, threshold = get_roc_for_fpr(attack_proba=bb_mia_test_probs, attack_true=bb_mia_test_labels, targeted_fpr=0.01)[0]\n",
    "print(f'{tpr=}: {fpr=}: {threshold=}')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "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\n",
      "text/plain": [
       "<Figure size 432x288 with 1 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    }
   ],
   "source": [
    "from sklearn.metrics import roc_curve\n",
    "import matplotlib.pyplot as plt\n",
    "\n",
    "fpr, tpr, _ = roc_curve(y_score=bb_mia_test_probs, y_true=bb_mia_test_labels)\n",
    "plt.figure()\n",
    "plt.plot(fpr, tpr, color=\"darkorange\", linewidth =2, label=\"ROC curve\")\n",
    "plt.plot([0, 1], [0, 1], color=\"navy\", linewidth =2, linestyle=\"--\", label='No skills')\n",
    "plt.xlim([0.0, 1.0])\n",
    "plt.ylim([0.0, 1.0])\n",
    "plt.xlabel(\"False Positive Rate\")\n",
    "plt.ylabel(\"True Positive Rate\")\n",
    "plt.title(\"Receiver operating characteristic example\")\n",
    "plt.legend(loc=\"lower right\")\n",
    "plt.show()\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Worst Case per Class evaluation for Black-Box Attack"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Class 0: tpr=0.0118: fpr=0.0000: threshold=0.9900\n",
      "Class 1: tpr=0.1667: fpr=0.0000: threshold=0.9600\n",
      "Class 2: tpr=0.0000: fpr=0.0000: threshold=1.9800\n",
      "Class 3: tpr=0.0149: fpr=0.0000: threshold=0.9800\n"
     ]
    }
   ],
   "source": [
    "# We allow 1% FPR \n",
    "target_labels = np.concatenate((y_train[attack_train_size:], y_test[attack_test_size:]))\n",
    "results = get_roc_for_fpr(attack_proba=bb_mia_test_probs, attack_true=bb_mia_test_labels, targeted_fpr=0.01, target_model_labels=target_labels)\n",
    "for v, fpr, tpr, threshold in results:\n",
    "    print(f'Class {v}: {tpr=:.4f}: {fpr=:.4f}: {threshold=:.4f}')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.8.5"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
